import GLib from 'gi://GLib';
import GnomeDesktop from 'gi://GnomeDesktop';
+import Meta from 'gi://Meta';
+
+import * as Signals from './signals.js';
export const DEFAULT_LOCALE = 'en_US';
export const DEFAULT_LAYOUT = 'us';
return _keyboardManager;
}
-class KeyboardManager {
+class KeyboardManager extends Signals.EventEmitter {
constructor() {
+ super();
+
// The XKB protocol doesn't allow for more than 4 layouts in a
// keymap. Wayland doesn't impose this limit and libxkbcommon can
// handle up to 32 layouts but since we need to support X clients
this._localeLayoutInfo = this._getLocaleLayout();
this._layoutInfos = {};
this._currentKeymap = null;
+
+ global.backend.connect('keymap-changed', this._onKeymapChanged.bind(this));
+ global.backend.connect('keymap-layout-group-changed', this._onKeymapLayoutGroupChanged.bind(this));
+ global.backend.connect('reset-keymap-description',
+ () => this._ourKeymapDescription);
+ global.backend.connect('reset-keymap-layout-index',
+ () => this._current.groupIndex);
}
- async _applyLayoutGroup(group) {
+ _updateCurrentKeymap(info) {
const options = this._buildOptionsString();
- const [layouts, variants] = this._buildGroupStrings(group);
+ const [layouts, variants] = this._buildGroupStrings(info.group);
const model = this._xkbModel;
if (this._currentKeymap &&
this._currentKeymap.variants === variants &&
this._currentKeymap.options === options &&
this._currentKeymap.model === model)
- return;
+ return false;
+
+ const displayNames = info.group.map(g => g.displayName);
+ const shortNames = info.group.map(g => g.shortName);
+ this._currentKeymap = {
+ layouts,
+ variants,
+ options,
+ model,
+ displayNames,
+ shortNames,
+ };
+ return true;
+ }
- this._currentKeymap = {layouts, variants, options, model};
- await global.backend.set_keymap_async(layouts, variants, options, model, null);
+ _createKeymapDescription() {
+ return Meta.KeymapDescription.new_from_rules(this._currentKeymap.model,
+ this._currentKeymap.layouts,
+ this._currentKeymap.variants,
+ this._currentKeymap.options,
+ this._currentKeymap.displayNames,
+ this._currentKeymap.shortNames
+ );
}
- async _applyLayoutGroupIndex(idx) {
- await global.backend.set_keymap_layout_group_async(idx, null);
+ _onKeymapChanged() {
+ this._keymapDescription = global.backend.get_keymap_description();
+ this.emit('keymap-changed');
}
- async _doApply(info) {
- await this._applyLayoutGroup(info.group);
- await this._applyLayoutGroupIndex(info.groupIndex);
+ _onKeymapLayoutGroupChanged() {
+ this.emit('keymap-changed');
}
- apply(id) {
+ async _doApply(id) {
const info = this._layoutInfos[id];
if (!info)
return;
- if (this._current && this._current.group === info.group) {
- if (this._current.groupIndex !== info.groupIndex)
- this._applyLayoutGroupIndex(info.groupIndex).catch(logError);
- } else {
- this._doApply(info).catch(logError);
+ let recreate;
+ if (this._updateCurrentKeymap(info))
+ recreate = true;
+ else if (this.isExternal())
+ recreate = true;
+ else
+ recreate = false;
+
+ if (recreate)
+ this._ourKeymapDescription = this._createKeymapDescription();
+
+ if (recreate || !this._current || this._current.groupIndex !== info.groupIndex) {
+ await global.backend.set_keymap_async(
+ this._ourKeymapDescription, info.groupIndex, null);
}
this._current = info;
}
+ apply(id) {
+ this._doApply(id).catch(logError);
+ }
+
reapply() {
if (!this._current)
return;
this._layoutInfos = {};
for (const id of ids) {
- const [found, , , layout, variant] = this._xkbInfo.get_layout_info(id);
- if (found)
- this._layoutInfos[id] = {id, layout, variant};
+ const [found, displayName, shortName, layout, variant] =
+ this._xkbInfo.get_layout_info(id);
+ if (found) {
+ this._layoutInfos[id] = {
+ id,
+ layout,
+ variant,
+ displayName,
+ shortName,
+ };
+ }
}
let i = 0;
get currentLayout() {
return this._current;
}
+
+ get shortName() {
+ const seat = global.stage.context.get_backend().get_default_seat();
+ const keymap = seat.get_keymap();
+ return keymap.get_current_short_name();
+ }
+
+ get displayName() {
+ const seat = global.stage.context.get_backend().get_default_seat();
+ const keymap = seat.get_keymap();
+ return keymap.get_current_display_name();
+ }
+
+ isLocked() {
+ return this._keymapDescription?.is_locked() ?? false;
+ }
+
+ isExternal() {
+ if (!this._keymapDescription)
+ return false;
+ if (!this._ourKeymapDescription)
+ return true;
+ return !this._keymapDescription.direct_equal(this._ourKeymapDescription);
+ }
}
_init(displayName, shortName) {
super._init();
- this.setOrnament(PopupMenu.Ornament.NO_DOT);
-
this.label = new St.Label({
text: displayName,
x_expand: true,
this._xkbInfo = KeyboardManager.getXkbInfo();
this._keyboardManager = KeyboardManager.getKeyboardManager();
+ this._keyboardManager.connect('keymap-changed', this._keymapChanged.bind(this));
this._ibusReady = false;
this._ibusManager = IBusManager.getIBusManager();
}
_switchInputSource(display, window, event, binding) {
+ if (this._keyboardManager.isLocked())
+ return;
+
if (this._mruSources.length < 2)
return;
this._keyboardManager.reapply();
}
+ _keymapChanged() {
+ this.emit('keymap-changed');
+ }
+
_updateMruSettings() {
// If IBus is not ready we don't have a full picture of all
// the available sources, so don't update the setting
this._inputSourceManager = getInputSourceManager();
this._inputSourceManager.connectObject(
'sources-changed', this._sourcesChanged.bind(this),
- 'current-source-changed', this._currentSourceChanged.bind(this), this);
+ 'current-source-changed', this._currentSourceChanged.bind(this),
+ 'keymap-changed', this._keymapChanged.bind(this), this);
this._inputSourceManager.reload();
}
this._showLayoutItem.visible = Main.sessionMode.allowSettings;
}
- _sourcesChanged() {
- for (const i in this._menuItems)
- this._menuItems[i].destroy();
- for (const i in this._indicatorLabels)
- this._indicatorLabels[i].destroy();
+ _createExternalSource(keyboardManager) {
+ const {displayName, shortName} = keyboardManager;
+ const is = new InputSource('external', 'external', displayName, shortName, -1);
+ is.locked = keyboardManager.isLocked();
+ return is;
+ }
- this._menuItems = {};
- this._indicatorLabels = {};
+ _getCurrentSource() {
+ const {keyboardManager} = this._inputSourceManager;
+ if (keyboardManager.isExternal())
+ return this._inputSources[0];
+ else
+ return this._inputSourceManager.currentSource;
+ }
+
+ _updateInputSources() {
+ const {keyboardManager} = this._inputSourceManager;
+ if (keyboardManager.isLocked()) {
+ const is = this._createExternalSource(keyboardManager);
+ this._inputSources = [is];
+ return;
+ }
- let menuIndex = 0;
- for (const i in this._inputSourceManager.inputSources) {
- const is = this._inputSourceManager.inputSources[i];
+ const inputSources = [];
+ if (keyboardManager.isExternal())
+ inputSources.push(this._createExternalSource(keyboardManager));
+
+ const internalSources = this._inputSourceManager.inputSources;
+ inputSources.push(
+ ...Object.keys(internalSources).sort((a, b) => a - b).map(i => internalSources[i]));
+
+ this._inputSources = inputSources;
+ }
+
+ _addSourceIndicators() {
+ const inputSources = this._inputSources;
+ for (const i in inputSources) {
+ const is = inputSources[i];
const menuItem = new LayoutMenuItem(is.displayName, is.shortName);
- menuItem.connect('activate', () => is.activate(true));
+ if (is.type !== 'external')
+ menuItem.connect('activate', () => is.activate(true));
+ else
+ menuItem.sensitive = false;
+
+ if (is.locked)
+ menuItem.setOrnament(PopupMenu.Ornament.HIDDEN);
+ else
+ menuItem.setOrnament(PopupMenu.Ornament.NO_DOT);
const indicatorLabel = new St.Label({
text: is.shortName,
visible: false,
});
- this._menuItems[i] = menuItem;
- this._indicatorLabels[i] = indicatorLabel;
+ const indicatorIndex = this._calculateSourceIndicatorIndex(is);
+
+ this._menuItems[indicatorIndex] = menuItem;
+ this._indicatorLabels[indicatorIndex] = indicatorLabel;
is.connect('changed', () => {
menuItem.indicator.set_text(is.shortName);
indicatorLabel.set_text(is.shortName);
});
- this.menu.addMenuItem(menuItem, menuIndex++);
+ this.menu.addMenuItem(menuItem, indicatorIndex);
this._container.add_child(indicatorLabel);
}
}
+ _sourcesChanged() {
+ this._updateInputSources();
+
+ for (const i in this._menuItems)
+ this._menuItems[i].destroy();
+ for (const i in this._indicatorLabels)
+ this._indicatorLabels[i].destroy();
+
+ this._menuItems = {};
+ this._indicatorLabels = {};
+
+ this._addSourceIndicators();
+ }
+
+ _calculateSourceIndicatorIndex(source) {
+ const keyboardManager = this._inputSourceManager.keyboardManager;
+ return (keyboardManager.isExternal() ? 1 : 0) + source.index;
+ }
+
+ _setSourceAsActive(source) {
+ const index = this._calculateSourceIndicatorIndex(source);
+ if (!source.locked)
+ this._menuItems[index]?.setOrnament(PopupMenu.Ornament.DOT);
+ this._indicatorLabels[index]?.show();
+ }
+
+ _setSourceAsInactive(source) {
+ const index = this._calculateSourceIndicatorIndex(source);
+ if (!source.locked)
+ this._menuItems[index]?.setOrnament(PopupMenu.Ornament.NO_DOT);
+ this._indicatorLabels[index].hide();
+ }
+
_currentSourceChanged(manager, oldSource) {
- const nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length;
- const newSource = this._inputSourceManager.currentSource;
+ const nVisibleSources = Object.keys(this._inputSources).length;
+ const newSource = this._getCurrentSource();
- if (oldSource) {
- this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NO_DOT);
- this._indicatorLabels[oldSource.index].hide();
- }
+ if (oldSource)
+ this._setSourceAsInactive(oldSource);
if (!newSource || (nVisibleSources < 2 && !newSource.properties)) {
// This source index might be invalid if we weren't able
this._buildPropSection(newSource.properties);
- this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT);
- this._indicatorLabels[newSource.index].show();
+ this._setSourceAsActive(newSource);
+ }
+
+ _keymapChanged() {
+ this._sourcesChanged();
+ this._setSourceAsActive(this._getCurrentSource());
}
_buildPropSection(properties) {
else
text = prop.get_label().get_text();
- const currentSource = this._inputSourceManager.currentSource;
+ const currentSource = this._getCurrentSource();
if (currentSource) {
- const indicatorLabel = this._indicatorLabels[currentSource.index];
+ const index = this._calculateSourceIndicatorIndex(currentSource);
+ const indicatorLabel = this._indicatorLabels[index];
const graphemeClusters = this._getGraphemeClusters(text);
if (graphemeClusters.length > 0 && graphemeClusters.length < 3)
indicatorLabel.set_text(text);